/**
 * 
 */
package com.code.google.p.jyve;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.FontMetrics;
import java.awt.GradientPaint;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Stroke;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.GeneralPath;
import java.util.ArrayList;

import javax.swing.JPanel;

import com.code.google.p.jyve.berekeningen.AdvancedGuardDag;
import com.code.google.p.jyve.berekeningen.Berekening;
import com.code.google.p.jyve.indicatoren.Indicator;

/** Class voor het tekenen van de grafiek.
 * @author Boudewijn
 */
public final class Graph extends JPanel implements MouseWheelListener {
	
	private static final Color GRID_COLOR = Color.GRAY;
	private static final Color BOUNDS_COLOR = Color.BLACK;
	private static final Color BACKGROUND_COLOR = Color.WHITE;
	
	private static final float INDICATOR_TRANSPERANCY = 0.5f;
	
	private static final float LINE_WIDTH = 1.5f;
	
	private static final float LINE_WIDTH_PH_7 = 5f;
	
	private static final Class<?> DEFAULT_GUARD_DOG = SimpleGuardDog.class;
	
	/**
	 * True will drawing graph pixel by pixel, false will use path
	 */
	private static final boolean FASTER_GRAPH_DRAWING = false;
	
	
	private ArrayList<Berekening> mBerekeningen = new ArrayList<Berekening>();
	
	private ArrayList<Indicator> mIndicatoren = new ArrayList<Indicator>();
	
	private GuardDog mGuardDog;
	
	private int mGraphWidth;
	private int mGraphHeight;
	
	private int mGraphMarginLeft = 40;
	private int mGraphMarginRight = 10;
	private int mGraphMarginTop = 10;
	private int mGraphMarginBottom = 40;
	
	private double mMinML = 0d;
	private double mMaxML = 30d;
	
	private double mMaxPH = 14d;
	private double mMinPH = 0d;
	
	private double mScaleStepX = 1;
	private double mScaleStepY = 1;
	
	private boolean mDrawGrid = true;
	
	/**
	 * 
	 */
	private static final long serialVersionUID = -2503275998387758166L;
	
	public Graph(){
		addMouseWheelListener(this);
	}
	
	@Override
	public void mouseWheelMoved(MouseWheelEvent event) {
		// TODO In en uit zoomen
	}

	@Override
	protected void processMouseEvent(MouseEvent e) {
		// TODO Muis over lijn, dan gegevens weergeven. Nog bedenken wat er moet gebeuren.
		super.processMouseEvent(e);
	}
	
	
	// Gebied voor tekenen
	@Override
	protected void paintComponent(Graphics graphics) {
		Graphics2D g = (Graphics2D) graphics;
		g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
		BasicStroke stroke = new BasicStroke(1);
		g.setStroke(stroke);
		
		
		Rectangle bounds = getBounds();
		mGraphWidth = (bounds.width - mGraphMarginLeft) - mGraphMarginRight;
		mGraphHeight = (bounds.height - mGraphMarginBottom) - mGraphMarginTop;
		
		g.setColor(BACKGROUND_COLOR);
		g.fillRect(0, 0, bounds.width, bounds.height);
		g.translate(mGraphMarginLeft, mGraphMarginTop);
		g.clipRect(0, 0, mGraphWidth, mGraphHeight);
		if (mDrawGrid) drawGrid(g);
		BasicStroke graphStroke = new BasicStroke(LINE_WIDTH);
		g.setStroke(graphStroke);
		drawGraphs(g);
		g.setStroke(stroke);
		drawIndicatoren(g);
		g.setStroke(stroke);
		g.setClip(-1 * (mGraphMarginLeft), -1 * (mGraphMarginTop), bounds.width, bounds.height);
		drawBounds(g);		
	}
	
	

	private void drawGrid(Graphics2D g) {
		g.setColor(GRID_COLOR);	
		
		for (int x = (int)Math.ceil(mMinML); x <= Math.floor(mMaxML); x += mScaleStepX){
			int pixel = Math.min(mGraphWidth - 1, mLToPixel(x));
			g.drawLine(pixel, 0, pixel, mGraphHeight);
		}
		for (int y = (int)Math.ceil(mMinPH); y <= Math.floor(mMaxPH); y += mScaleStepY){
			if (y == 7){
				Stroke old = g.getStroke();
				BasicStroke thicker = new BasicStroke(LINE_WIDTH_PH_7); 
				g.setStroke(thicker);
			int pixel = pHToPixel(y);
			g.drawLine(0, pixel, mGraphWidth, pixel);
			g.setStroke(old);
			}
			else {
				int pixel = pHToPixel(y);
				g.drawLine(0, pixel, mGraphWidth, pixel);
			}
		}
	}
	
	private void drawGraphs(Graphics2D g) {
		if (mGuardDog == null){
			System.out.println("Waarschuwing: geen waakhond bepaald. Gaat naar standaard.");
			try {
				mGuardDog = (GuardDog) DEFAULT_GUARD_DOG.newInstance();
			} catch (InstantiationException e) {
				e.printStackTrace();
			} catch (IllegalAccessException e) {
				e.printStackTrace();
			}
		}
		// Niet de enhanced for loop gebruiken, omdat deze sneller is.
		int length = mBerekeningen.size();
		for (int i = 0; i < length; i++){
			drawGraph(g, mBerekeningen.get(i));
		}
	}
	
	private void drawGraph(Graphics2D g, Berekening berekening){
		g.setColor(berekening.getPreferredColor());
		double[] input = mGuardDog.suggest(berekening, mGraphWidth / (mMaxML - mMinML), mGraphHeight / (mMaxPH - mMinPH), mMaxML);
		if (FASTER_GRAPH_DRAWING){
			for (int x = 0; x < input.length; x++){				
				double ph = berekening.berekenVanuitTitrant(input[x]);
				int pointY = pHToPixel(ph);
				g.drawRect(mLToPixel(input[x]), (int)pointY, 0, 0);			
			}		
		}
		else {
			GeneralPath path = new GeneralPath();
			path.moveTo(0, pHToPixelExact(berekening.berekenVanuitTitrant(0)));
			for (int x = 0; x < input.length; x++){				
				double ph = berekening.berekenVanuitTitrant(input[x]);
				double pointY = pHToPixelExact(ph);
				path.lineTo(mLToPixelExact(input[x]), pointY);			
			}
			g.draw(path);
		}
	}
	
	private void drawIndicatoren(Graphics2D g) {
		int length = mIndicatoren.size();
		for (int i = 0; i < length; i++){
			drawIndicator(g, mIndicatoren.get(i));
		}
	}
	
	private void drawIndicator(Graphics2D g, Indicator indicator){
		Color begin = indicator.getKleurBegin();
		begin = new Color(begin.getRed(), begin.getGreen(), begin.getBlue(), Math.round(begin.getAlpha() * INDICATOR_TRANSPERANCY));
		Color end = indicator.getKleurEind();
		end = new Color(end.getRed(), end.getGreen(), end.getBlue(), Math.round(end.getAlpha() * INDICATOR_TRANSPERANCY));
		GradientPaint paint = new GradientPaint(0, (float)pHToPixelExact(indicator.getBeginPH()), begin, 0, (float)pHToPixelExact(indicator.getEindPH()), end);
		Paint temp = g.getPaint();
		g.setPaint(paint);
		
		int beginY = pHToPixel(indicator.getBeginPH());
		int endY = pHToPixel(indicator.getEindPH());
		
		g.fillRect(0, endY, mGraphWidth, (beginY - endY) + 1); // Hoogte gecorrigeerd, om te zorgen dat hij klopt volgens het grid.
		g.setPaint(temp);
		g.setColor(end);
		g.setStroke(new BasicStroke(2));
		g.drawLine(0, beginY, mGraphWidth, beginY);
		g.drawLine(0, endY, mGraphWidth, endY);
	}
	
	private void drawBounds(Graphics2D g) {
		g.setColor(BOUNDS_COLOR);
		
		// TODO Misschien nog aanpassen imv. grote aantal lijnen bij kleine stapgrootte
		g.drawLine(0, mGraphHeight, mGraphWidth - 1, mGraphHeight);
		g.drawLine(0, 0, 0, mGraphHeight);
		
		FontMetrics metrics = g.getFontMetrics();
		
		for (int x = (int)Math.ceil(mMinML); x <= Math.floor(mMaxML); x += mScaleStepX){
			int pixel = mLToPixel(x);
			String current = String.valueOf(x);
			g.drawString(current, pixel - (metrics.stringWidth(current) / 2), mGraphHeight + metrics.getHeight());
		}
		for (int y = (int)Math.ceil(mMinPH); y <= Math.floor(mMaxPH); y += mScaleStepY){
			int pixel = pHToPixel(y);
			String current = String.valueOf(y);
			g.drawString(current, (-1 * (metrics.stringWidth(current))) - 5, pixel + (metrics.getAscent() / 2));
		}
		g.drawString("pH", -mGraphMarginLeft, metrics.getAscent() / 2);
		String textML = "Hoeveelheid titrant (mL)";
		g.drawString(textML, mGraphWidth - metrics.stringWidth(textML), mGraphHeight + mGraphMarginBottom - (metrics.getAscent() / 2));
	}
	
	
	
	// Gebied voor externe manipulatie indicatoren
	public void addIndicator(Indicator indicator){
		mIndicatoren.add(indicator); 
		repaint();
	}	

	public boolean containsIndicator(Indicator indicator){
		return mIndicatoren.contains(indicator);	
	}

	public void removeIndicator(Indicator indicator){
		mIndicatoren.remove(indicator);
		repaint();
	}
	
	// Gebied voor externe manipulatie berekeningen
	public void addBerekening(Berekening berekening) {
		mBerekeningen.add(berekening);
		repaint();
	}

	public boolean containsBerekening(Berekening berekening) {
		return mBerekeningen.contains(berekening);
	}

	public void removeBerekening(Berekening berekening) {
		mBerekeningen.remove(berekening);
		repaint();
	}
	
	public void setGuardDog(GuardDog guardDog){
		mGuardDog = guardDog;
		repaint();
	}
	
	/**
	 * Bepaal het bereik van de grafiek, hiermee wordt de horizontale as aangepast.
	 * @param minML Het nieuwe minimum van de hoeveelheid titrant.
	 * @param maxML Het nieuwe maximum van de hoeveelheid titrant.
	 * @throw IllegalArgumentException als minML en maxML gelijk zijn.
	 */
	public void setMLRange(double minML, double maxML){
		if (minML == maxML) throw new IllegalArgumentException("minML en maxML zijn gelijk, dit kan niet");
		mMinML = minML;
		mMaxML = maxML;
		repaint();
	}
	
	/**
	 * Bepaal het bereik van de grafiek, hiermee wordt de verticale as aangepast.
	 * @param minPH Het nieuwe minimum van de pH.
	 * @param maxPH Het nieuwe maximum van de pH.
	 * @throws IllegalArgumentException als minPH en maxPH gelijk zijn.
	 */
	public void setPHRange(double minPH, double maxPH){
		if (minPH == maxPH) throw new IllegalArgumentException("minPH en maxPH zijn gelijk, dit kan niet");
		mMinPH = minPH;
		mMaxPH = maxPH;
		repaint();
	}
	
	/**
	 * Hiermee krijg je de laagste hoeveelheid terug van de titrant.
	 * @return De laagste hoeveelheid titrant waarmee wordt gerekend.
	 */
	public double getMinML(){
		return mMinML;
	}
	
	/**
	 * Hiermee krijg je de hoogste hoeveelheid terug van de titrant.
	 * @return De hoogste hoeveelheid titrant waarmee wordt gerekend.
	 */
	public double getMaxML(){
		return mMaxML;
	}
	
	/**
	 * Hiermee krijg je de laagste pH terug.
	 * @return De laagst pH die op de grafiek kan worden weergegeven.
	 */
	public double getMinPH(){
		return mMinPH;
	}
	
	/**
	 * Hiermee krijg je de maximale pH terug.
	 * @return De hoogste pH die op de grafiek kan worden weergegeven.
	 */
	public double getMaxPH(){
		return mMaxPH;
	}
	
	/**
	 * Helper methode voor de setMLRange en setPHRange, deze is echter sneller en moet dus
	 * indien mogelijk gebruikt. Bepaalt dus het bereik van zowel de horizontale en de verticale as.
	 * Hiervoor zijn respectivelijk de mL en de pH.
	 * @param minML Het nieuwe minimum van de hoeveelheid titrant.
	 * @param maxML Het nieuwe maximum van de hoeveelheid titrant.
	 * @param minPH Het nieuwe minimum van de pH.
	 * @param maxPH Het nieuwe maximum van de pH.
	 */
	public void setRange(double minML, double maxML,
			double minPH, double maxPH){
		if (minML == maxML) throw new IllegalArgumentException("minML en maxML zijn hetzelfde, mag niet");
		if (minPH == maxPH) throw new IllegalArgumentException("minPH en maxPH zijn hetzelfde, mag niet");
		mMinML = minML;
		mMaxML = maxML;
		mMinPH = minPH;
		mMaxPH = maxPH;
		repaint();
	}
	
	/**
	 * Het tekenen van het grid aan- en uitzetten.
	 * @param on Of het grid wel of niet getekend moet worden. True voor wel getekend. 
	 * False voor niet tekenen.
	 */
	public void setGrid(boolean on){
		mDrawGrid = on;
		repaint();
	}
	
	/**
	 * Krijg terug of het grid wel of niet getekend wordt.
	 * @return Of het grid wel of niet getekend wordt. True voor wel, false voor niet.
	 */
	public boolean getGrid(){
		return mDrawGrid;
	}
	
	public void setStep(double xStep, double yStep){
		mScaleStepX = xStep;
		mScaleStepY = yStep;
	}
	
	public double getStepX(){
		return mScaleStepX;
	}
	
	public double getStepY(){
		return mScaleStepY;
	}
	
	// Gebied voor omzetten waardes naar pixel en vice versa
	private int pHToPixel(double pH){
		int pointY = (int) (((pH - mMinPH) / (mMaxPH - mMinPH)) * mGraphHeight);
		pointY = mGraphHeight - pointY;
		return pointY;
	}

	private int mLToPixel(double mL){
		int pointX = (int) (((mL - mMinML) / (mMaxML - mMinML)) * mGraphWidth);
		return pointX;
	}
	
	private double pHToPixelExact(double pH){
		double pointY = (((pH - mMinPH) / (mMaxPH - mMinPH)) * mGraphHeight);;
		pointY = mGraphHeight - pointY;
		return pointY;
	}
	
	private double mLToPixelExact(double mL){
		return (((mL - mMinML) / (mMaxML - mMinML)) * mGraphWidth);
	}

	@SuppressWarnings("unused") // Possibly used in future
	private double pixelToPh(int pixel){
		double ph = (((double)pixel / mGraphHeight) * (mMaxML - mMinML)) + mMinML;
		return ph;
	}

	@SuppressWarnings("unused") // Possibly used in future
	private double pixelToML(int pixel){
		double titrant = (((double)(pixel) / mGraphWidth) * (mMaxML - mMinML)) + mMinML;
		return titrant;
	}
	
	public interface GuardDog {
		
		/**
		 * Wordt aangeroepen om een set gegevens terug te geven, die in de berekening wordt ingevoerd.
		 * Het doel hiervan is om de zorgen dat er een mooie grafiek uitkomt. Hij moet zo dicht mogelijk bij 14 uitkomen
		 * @param berekening De berekening waarvoor de invoer moet worden berekent.
		 * @param xPrecision Het aantal pixels per mL.
		 * @param yPrecision Het aantal pixels per pH.
		 * @param xEnd Het aantal mL waar hij moet stoppen met 
		 * @return Een array met daarin de waardes die berekent moeten worden.
		 */
		public double[] suggest(Berekening berekening, double xPrecision, double yPrecision, double xEnd);
	
	}
}
